Skip to content

feat(action): local bindings + Output accessors in Actions#807

Merged
sam-goodwin merged 15 commits into
mainfrom
claude/alchemy-actions-local-bindings-825ac1
Jul 16, 2026
Merged

feat(action): local bindings + Output accessors in Actions#807
sam-goodwin merged 15 commits into
mainfrom
claude/alchemy-actions-local-bindings-825ac1

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Actions can now bind resources with the current credentials and read resource Outputs, the same way a Cloudflare.Worker does.

const Seed = Alchemy.Action(
  "Seed",
  Effect.gen(function* () {
    const db = yield* Cloudflare.D1.QueryDatabase(database);
    const databaseId = yield* database.databaseId; // accessor captured at init

    return Effect.fn(function* () {
      yield* db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)");
      yield* db
        .prepare("INSERT INTO users (id, name) VALUES (?, ?)")
        .bind("1", "Ada")
        .run();
      return { databaseId: yield* databaseId };
    });
  }).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseLocal)),
);

*Local binding layers

A third binding variant alongside *Binding (native Worker) and *Http (scoped token). A *Local layer resolves the current CLI credentials (ambient during stack-eval via the stack's providers context), never touches a Worker host or host.bind, and returns the same client backed by the provider's HTTP API. Where an *Http variant already exists, its client builder is refactored to take an injectable { authorize, accountId } auth shared by both the token-scoped Http and the current-creds Local; otherwise the Local layer implements a direct HTTP client (D1's shim is the reference).

Implemented for every Cloudflare capability with an HTTP data-plane — each live-tested against real Cloudflare:

Service Local layers
D1 QueryDatabaseLocal
KV ReadNamespaceLocal / WriteNamespaceLocal / ReadWriteNamespaceLocal
R2 ReadBucketLocal / WriteBucketLocal / ReadWriteBucketLocal
Queues WriteQueueLocal
DNS ReadDnsLocal / WriteDnsLocal / ReadWriteDnsLocal
Vectorize SearchIndexLocal
Tunnel ReadTunnelLocal / WriteTunnelLocal / ReadWriteTunnelLocal
AI Search QuerySearchLocal / QuerySearchNamespaceLocal
Flagship ReadFlagsLocal
Workers BrowserLocal (Browser Rendering REST API)

Worker-runtime-only bindings have no Local variant and are intentionally excluded (no account HTTP data-plane): SecretsStore (write-only secret values), AnalyticsEngine (write-only ingestion), Images (in-Worker transforms), Hyperdrive (connection pooler), Email (send_email), Addressing, Artifacts, WorkersForPlatforms dispatch, AI QueryGateway (inference), RateLimit, VersionMetadata, and service bindings.

Output accessors in Actions

An Action's init runs at stack-eval (before resources exist); its body runs at apply. To make yield* db.databaseId work, RuntimeContext is split into two cooperating halves:

  • a capture context provided around the init — records referenced Outputs and hands back deferred accessors
  • a resolve context provided around the body — returns values evaluated against the tracker

Captured Outputs also become dependency edges, so the Action waits for the bound resource. Works in both the inline form and the tagged .make form.

Verification

  • test/action.test.ts — inline + tagged .make capture/accessor tests.
  • One live *Local test per implemented service (deploy resource + Action driving the Local binding).
  • Workspace bun tsc -b clean.

Follow-up

Vectorize insertIndex/upsertIndex are generated with a multipart part named body, but the v2 API requires a part named vectors; SearchIndexLocal works around it in-layer. A distilled generator/spec fix would let every consumer get the correct multipart field for free.

Actions can now bind resources with current credentials and read
resource Outputs, like a Cloudflare Worker.

- ActionRuntimeContext: `yield* db.databaseId` inside an Action init
  captures the Output (→ dependency edge) and resolves it against the
  tracker at apply time. Works in both the inline and tagged `.make`
  forms.
- Cloudflare.D1.QueryDatabaseLocal: a `*Local` layer (third variant
  alongside `*Binding`/`*Http`) that queries D1 over HTTP with the
  current credentials — no Worker host, no `host.bind`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alchemy-version-bot

alchemy-version-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/1eef6b5

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/1eef6b5

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/1eef6b5

sam-goodwin and others added 4 commits July 11, 2026 00:22
*Local layers (current-credentials, over HTTP) for the KV, R2, and
Queues bindings so they can be used inside an Action. Each refactors
its shared *Http client builder to accept an injectable auth
({ authorize, accountId }) reused by both the token-scoped Http variant
and the current-creds Local variant.

- KV: ReadNamespaceLocal / WriteNamespaceLocal / ReadWriteNamespaceLocal
- R2: ReadBucketLocal / WriteBucketLocal / ReadWriteBucketLocal
- Queues: WriteQueueLocal

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h, Flagship, Browser

Extends the *Local (current-credentials, HTTP) binding pattern to every
Cloudflare capability with an HTTP data-plane, so they can be used inside
an Action:

- DNS:       ReadDnsLocal / WriteDnsLocal / ReadWriteDnsLocal
- Vectorize: SearchIndexLocal
- Tunnel:    ReadTunnelLocal / WriteTunnelLocal / ReadWriteTunnelLocal
- AI Search: QuerySearchLocal / QuerySearchNamespaceLocal
- Flagship:  ReadFlagsLocal
- Workers:   BrowserLocal (Browser Rendering REST API)

Each reuses/refactors its *Http client builder via an injectable
{ authorize, accountId } auth (shared by token-scoped Http and
current-creds Local), or a direct HTTP client where no *Http existed.
Every layer is live-tested against real Cloudflare.

Worker-runtime-only bindings have no Local variant and are intentionally
excluded: SecretsStore (write-only values), AnalyticsEngine (write-only
ingestion), Images (in-Worker transforms), Hyperdrive (pooler), Email
(send_email), Addressing, Artifacts, WorkersForPlatforms dispatch, AI
QueryGateway, RateLimit, VersionMetadata, service bindings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mData shim

Now that distilled models the vectorize insert/upsert multipart part as
`vectors`, SearchIndexLocal calls `vectorize.insertIndex`/`upsertIndex`
directly instead of hand-building a FormData request. Bumps the distilled
submodule to the regenerated service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rm .make

- Add "Binding resources" section: bind a resource inside an Action via
  its *Local layer (current-credentials HTTP), with the D1 seed example.
- Add "Reading a resource's Outputs": `yield* resource.attr` accessors
  resolved at apply time + the dependency edge they create.
- Fix the tagged form example: use the value form (interface + const),
  not the non-constructable `class extends` shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sam-goodwin and others added 2 commits July 15, 2026 12:59
…ocal

Distilled now models D1 query/raw `params` as `unknown[]`, so the Local
shim forwards native bind values (number/null/string) instead of failing
schema encoding. Mirror the native binding's normalization the raw HTTP
API skips: booleans -> 1/0, ArrayBuffer/views -> byte arrays (BLOB).

Adds single + batch coverage for numeric/null/boolean binds. Bumps the
distilled submodule to the params fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifies the ArrayBuffer/view -> byte-array normalization round-trips:
binding a Uint8Array and SELECT length(?) returns the blob byte count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sam-goodwin and others added 8 commits July 15, 2026 13:09
Rebases the D1 params + Vectorize multipart fixes onto distilled main
(companion PR alchemy-run/distilled#379), dropping the stale
claude/typescript-7-stable base so the submodule pointer no longer
conflicts with alchemy main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s-local-bindings-825ac1

# Conflicts:
#	distilled
Repoints the submodule to distilled 7d06469fc (companion PR
alchemy-run/distilled#379): D1 params are now a precise value union
`string | number | null | number[]` instead of unknown[].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Distilled now models vectorize insert/upsert as a raw application/x-ndjson
body (companion PR alchemy-run/distilled#379) matching the Cloudflare SDK,
instead of the multipart-with-vectors workaround. SearchIndexLocal passes
the ndjson blob as `body`. Bumps the distilled submodule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the SearchIndexClient construction + shape adapters into unexported
SearchIndexHttpClient.ts, parameterized by an injectable SearchIndexAuth
({ authorize, accountId }) — same convention as KV/R2/Tunnel/AI. A future
token-scoped SearchIndexHttp layer reuses the builder; SearchIndexLocal is
now just the layer + auth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gship, Browser

Same convention as KV/R2/Tunnel/AI/Vectorize: move client construction into
unexported {Cap}HttpClient.ts scaffolding parameterized by an injectable
auth ({ authorize, accountId }), so a future token-scoped *Http layer reuses
the builder instead of duplicating it. The *Local layers are now just
layer + auth + deferred id accessor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sam-goodwin sam-goodwin merged commit 0da65a7 into main Jul 16, 2026
5 checks passed
@sam-goodwin sam-goodwin deleted the claude/alchemy-actions-local-bindings-825ac1 branch July 16, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant